package com.atguigu.gmall.order.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.common.constant.RabbitConst;
import com.atguigu.gmall.common.service.RabbitService;
import com.atguigu.gmall.model.enums.OrderStatus;
import com.atguigu.gmall.model.enums.PaymentType;
import com.atguigu.gmall.model.enums.ProcessStatus;
import com.atguigu.gmall.model.order.OrderDetail;
import com.atguigu.gmall.model.order.OrderInfo;
import com.atguigu.gmall.order.mapper.OrderDetailMapper;
import com.atguigu.gmall.order.mapper.OrderInfoMapper;
import com.atguigu.gmall.order.service.api.OrderService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @author: 封捷
 * @create-date: 2022/9/8 10:29
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderInfoMapper orderInfoMapper;

    @Autowired
    private OrderDetailMapper orderDetailMapper;

    @Autowired
    private RabbitService rabbitService;

    @Override
    @Transactional
    public Long saveOrder(OrderInfo orderInfo, ProcessStatus processStatus) {

        // 1、保存 OrderInfo 信息
        // [1]计算总金额
        orderInfo.sumTotalAmount();

        // [2]设置订单状态
        orderInfo.setOrderStatus(OrderStatus.UNPAID.name());

        // [3]生成订单交易编号
        // 订单号设计原则：最关键的一点就是“不重复”
        // 我们采用的方案：公司名称 + 时间戳 + 随机数
        // 高并发的情况下，是否能保证订单号不重复？
        // 如果非要做到绝对不重复，那就再附加用户 id
        String outTradeNo = "MUCH_MONEY:" + orderInfo.getUserId() + ":" + System.currentTimeMillis() + new Random().nextInt(1000);

        // [4]设置订单交易编号
        orderInfo.setOutTradeNo(outTradeNo);

        // [5]拼接订单描述
        // ※思路：把商品名称拼接到一起即可
        // ※获取订单详情数据
        List<OrderDetail> orderDetailList = orderInfo.getOrderDetailList();

        // ※创建 StringBuffer 对象
        StringBuilder builder = new StringBuilder();

        // ※遍历订单详情集合
        for (OrderDetail orderDetail : orderDetailList) {
            String skuName = orderDetail.getSkuName();
            builder.append(skuName).append(",");
        }

        String tradeBody = StringUtils.trimTrailingCharacter(builder.toString(), ',');

        // ※检查 tradeBody 长度并截取（数据库表中，这个字段最多保存 200 个字符，所以不能超过这个限制）
        if (tradeBody.length() > 150) {
            // ※截取开头的一段
            tradeBody = tradeBody.substring(0, 150);
        }

        // [6]设置订单描述
        orderInfo.setTradeBody(tradeBody);

        // [7]设置订单创建的时间
        orderInfo.setCreateTime(new Date());

        // [8]设置订单过期时间（用户如果不在这个时间内支付，那么订单将会关闭）
        // ※生成过期时间
        // ※原则：商品库存充足
        // ※原则：商品库存短缺
        // ※我们这里就设置为 1 天的时间
        // ※ Calendar 刚创建出来的时候代表当前时间
        Calendar calendar = Calendar.getInstance();

        // ※ 让 Calendar 在当前时间的基础上再加 1 天
        calendar.add(Calendar.DATE, 1);

        // ※ 通过 Calendar 即可获取对应的日期时间（也就是一天以后的日期时间）
        Date expireTime = calendar.getTime();

        // ※设置过期时间
        orderInfo.setExpireTime(expireTime);

        // [9]设置进度状态
        // ※说明：ProcessStatus 和 OrderStatus 区别
        // ※ OrderStatus 就是订单本身的状态
        // ※ ProcessStatus 是更详细的状态，用于页面显示
        orderInfo.setProcessStatus(processStatus.getOrderStatus().name());

        // [10]插入数据库
        // ※执行数据库保存之后，自增的主键会被自动设置回 orderInfo 对象，赋值给 id 属性
        orderInfoMapper.insert(orderInfo);

        // 2、保存 OrderDetail 集合信息
        // [1]获取 orderId 值
        Long orderId = orderInfo.getId();

        // [2]对 List<OrderDetail> 集合做批量保存
        orderDetailMapper.batchInsert(orderId, orderDetailList);

        // 3、返回 orderId
        return orderId;
    }

    @Override
    public void updateOrderStatus(Long orderId, ProcessStatus processStatus) {
        orderInfoMapper.updateOrderStatus(
                orderId,
                processStatus.getOrderStatus().name(),
                processStatus.name());
    }

    // 被通用的方法替代，所以注释掉
//    @Override
//    public void closeOrder(Long orderId) {
//        orderInfoMapper
//                .updateOrderStatus(
//                        orderId,
//                        OrderStatus.CLOSED.name(),
//                        ProcessStatus.CLOSED.name());
//    }

    @Override
    public OrderInfo getOrderInfoById(Long orderId) {

        OrderInfo orderInfo = orderInfoMapper.selectById(orderId);

        // 由于后面我们做拆单功能时，需要订单详情数据的，所以这里我们把订单详情数据页查出来
        // ※说明1：其实这个做法在实际开发中有点不合适——不需要订单详情数据的场景也把订单详情查出来了，
        //          有点浪费内存
        // ※说明2：我们以前使用 resultMap 的方式访问数据库一次就能获取到关联数据，
        //          现在写法需要访问数据库两次，性能上稍差
        if (orderInfo != null) {
            List<OrderDetail> orderDetailList = orderDetailMapper
                    .selectList(
                            new QueryWrapper<OrderDetail>()
                                    .eq("order_id", orderId));

            orderInfo.setOrderDetailList(orderDetailList);
        }

        return orderInfo;
    }

    @Override
    public void notifyWareSystemToDeliver(Long orderId) {
        // 1、把订单状态中进程状态改成“已通知仓储”
        orderInfoMapper.updateOrderStatus(
                orderId,
                ProcessStatus.NOTIFIED_WARE.getOrderStatus().name(),
                ProcessStatus.NOTIFIED_WARE.name());

        // 2、把库存系统所需要的参数封装为 JSON 字符串格式
        String jsonArguments = notifyWareSystemToDeliverJSONArguments(orderId);

        // 3、给库存系统发送消息
        rabbitService.sendMessage(
                RabbitConst.EXCHANGE_DIRECT_WARE_STOCK,
                RabbitConst.ROUTING_WARE_STOCK,
                jsonArguments
            );

    }

    @Override
    public String doOrderSplit(Long orderId, String wareSkuMap) {

        // 1、根据 orderId 把被拆分的订单（原始订单）查询出来
        OrderInfo orderInfo = getOrderInfoById(orderId);

        // 2、把库存系统传过来的 JSON 字符串：wareSkuMap，转换为 Java 的类型
        // 库存系统文档给出的示例：[{"wareId":"1","skuIds":["2","10"]},{"wareId":"2","skuIds":["3"]}]
        // 最外层是 [] 说明它是 JSON 数组，对应的 Java 类型可以是：List
        // JSON 数组中每一个元素都是一个 JSON 对象
        // JSON 对象中 wareId 表示仓库 id，skuIds 表示当前订单里面，哪些商品属于这个仓库
        // JSON 对象可以对应 Java 类型：Map
        // 所以综合起来，总的类型是：List<Map>
        // 此时我们解析出来的这个 mapList 就是后续执行拆单操作的依据
        List<Map> mapList = JSON.parseArray(wareSkuMap, Map.class);

        // 3、把 OrderInfo 对象和 mapList 集合传给专门执行拆单逻辑的方法，返回我们最终需要的数据
        // subOrderInfoMap 代表一个子订单
        // 每一个子订单我们都封装为了一个 Map（封装为 Map 类型是为了最终数据结构更准确）
        // 最终数据是 Map 的 List 集合
        List<Map<String, Object>> subOrderInfoMapList = makeSubOrderMapList(orderInfo, mapList);

        // 4、把 subOrderInfoMapList 转换为 JSON 字符串
        String result = JSON.toJSONString(subOrderInfoMapList);

        // 5、更新主订单状态
        // ※说明：订单执行拆单操作之后，库存系统仅针对子订单发送减库存的消息
        // 对主订单就没有减库存的消息了，而我们修改订单状态是在减库存消息的消息接收器中执行
        // 现在主订单享受不到减库存消息的服务，所以需要我们自己在拆单后手动执行修改订单状态的操作
        updateOrderStatus(orderId, ProcessStatus.SPLIT);

        return result;
    }

    /**
     * 正式执行拆单逻辑
     * @param orderInfoOriginal
     * @param mapList
     * @return
     */
    private List<Map<String, Object>> makeSubOrderMapList(OrderInfo orderInfoOriginal, List<Map> mapList) {

        // 1、对 mapList 做判空保护
        if (CollectionUtils.isEmpty(mapList)) {
            return new ArrayList<>();
        }

        // 2、创建一个集合，用来保存最终的拆单结果
        List<Map<String, Object>> subOrderMapList = new ArrayList<>();

        // 3、遍历 mapList
        for (Map wareMap : mapList) {
            // 4、解析 wareMap
            // {"wareId":"1","skuIds":["2","10"]}
            // [1]获取仓库 id
            Object wareId = wareMap.get("wareId");

            // [2]获取 skuIds
            List<String> skuIdList = (List<String>) wareMap.get("skuIds");

            // 5、既然每一个 wareMap 都对应一个子订单，那么我们就可以创建 OrderInfo 对象代表这个子订单
            OrderInfo subOrderInfo = new OrderInfo();

            // 6、属性复制：从原始订单复制到子订单，
            // 这样原始订单和子订单一样的那部分数据就直接赋值到 subOrderInfo 对象中了
            BeanUtils.copyProperties(orderInfoOriginal, subOrderInfo);

            // 7、将来子订单也要保存到 order_info 表中，而原始订单也在这个表中
            // 所以订单 id 不能使用复制的值（避免和原始订单主键冲突），而是要把子订单的 id 置空——等待数据库主键自增
            subOrderInfo.setId(null);

            // 8、建立订单之间的父子关系
            // 原始订单：父
            // 子订单：子
            subOrderInfo.setParentOrderId(orderInfoOriginal.getId());

            // 9、获取原始的订单详情集合
            // ※注意：原始的订单详情集合中，各个 OrderDetail 对象，有的属于当前子订单，有的不属于
            // 假设：原始订单详情集合包括 2、10、13 这三个 SKU。
            // 其中 2、13 属于当前子订单
            List<OrderDetail> orderDetailListOriginal = orderInfoOriginal.getOrderDetailList();

            // 10、判空保护
            if (!CollectionUtils.isEmpty(orderDetailListOriginal)) {

                // 11、从原始订单详情集合中筛选出属于当前子订单的
                List<OrderDetail> subOrderDetailList = orderDetailListOriginal.stream().filter(orderDetail -> {

                    // 12、判断当前 orderDetail 是否属于当前子订单
                    // [1]获取当前 orderDetail 的 skuId
                    Long skuId = orderDetail.getSkuId();

                    // [2]判断 skuId 是否在 skuIdList 集合中
                    // ※千万别忘了：skuId 是 Long 类型，而 skuIdList 是 List<String>
                    // Long 和 String 即使实际的值一致也不会相等
                    return skuIdList.contains(skuId.toString());
                }).collect(Collectors.toList());

                // 13、把子订单的订单详情集合存入 subOrderInfo
                subOrderInfo.setOrderDetailList(subOrderDetailList);
            }

            // 14、计算当前子订单的总金额
            subOrderInfo.sumTotalAmount();

            // 15、给当前子订单设置仓库 id
            subOrderInfo.setWareId((String) wareId);

            // 16、把子订单保存到数据库中
            saveOrder(subOrderInfo, ProcessStatus.WAITING_DELEVER);

            // 17、把子订单对象转换为 Map 形式
            Map<String, Object> subOrderInfoMap = makeOrderInfoToMap(subOrderInfo);

            // 18、把子订单的 Map 对象存入最终结果集合
            subOrderMapList.add(subOrderInfoMap);
        }

        // 19、返回最终结果
        return subOrderMapList;
    }

    /**
     * 封装库存系统所需要的参数数据
     * @param orderId
     * @return JSON 格式字符串
     */
    private String notifyWareSystemToDeliverJSONArguments(Long orderId) {

        // 1、查询 OrderInfo 对象
        OrderInfo orderInfo = getOrderInfoById(orderId);

        // 2、把 OrderInfo 对象转换为 Map 类型
        Map<String, Object> jsonArgumentsMap = makeOrderInfoToMap(orderInfo);

        // 3、把 Map 类型的数据转换为 JSON 字符串
        return JSON.toJSONString(jsonArgumentsMap);
    }

    private Map<String, Object> makeOrderInfoToMap(OrderInfo orderInfo) {

        // 1、创建封装最终数据的 Map 集合
        Map<String, Object> argumentsMap = new HashMap<>();

        // 2、判空保护
        if (orderInfo == null) {
            return argumentsMap;
        }

        // 3、存入 OrderInfo 的基本属性
        argumentsMap.put("orderId", orderInfo.getId());
        argumentsMap.put("consignee", orderInfo.getConsignee());
        argumentsMap.put("consigneeTel", orderInfo.getConsigneeTel());
        argumentsMap.put("orderComment", orderInfo.getOrderComment());
        argumentsMap.put("orderBody", orderInfo.getTradeBody());
        argumentsMap.put("deliveryAddress", orderInfo.getDeliveryAddress());

        // 支付方式：  ‘1’ 为货到付款，‘2’为在线支付。
        argumentsMap.put("paymentWay", "2");

        // ※通知库存系统减库存的时候，没有 wareId
        // ※拆单后，子订单有 wareId，所以为了兼容拆单操作，这里也设置一下
        argumentsMap.put("wareId", orderInfo.getWareId());

        // 4、存入 List<OrderDetail> 集合数据
        // [1]获取订单详情集合数据
        List<OrderDetail> orderDetailList = orderInfo.getOrderDetailList();

        // [2]创建 List<Map<String,Object>> 集合，用于存放解析结果数据
        List<Map<String, Object>> orderDetailMapList = new ArrayList<>();

        // [3]遍历订单详情集合
        for (OrderDetail orderDetail : orderDetailList) {
            // [4]在遍历订单详情集合的过程中，把每一个订单详情对象转换为 Map<String,Object>
            Map<String, Object> orderDetailMap = new HashMap<>();
            orderDetailMap.put("skuId", orderDetail.getSkuId());
            orderDetailMap.put("skuNum", orderDetail.getSkuNum());
            orderDetailMap.put("skuName", orderDetail.getSkuName());

            // [5]把 Map<String,Object> 存入 List<Map<String,Object>> 集合
            orderDetailMapList.add(orderDetailMap);
        }

        // [6]把存放订单详情解析结果的集合存入最终集合
        argumentsMap.put("details", orderDetailMapList);

        return argumentsMap;
    }
}
